/*
 * The JTS Topology Suite is a collection of Java classes that
 * implement the fundamental operations required to validate a given
 * geo-spatial data set to a known topological specification.
 *
 * Copyright (C) 2001 Vivid Solutions
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * For more information, contact:
 *
 *     Vivid Solutions
 *     Suite #1A
 *     2328 Government Street
 *     Victoria BC  V8T 5G5
 *     Canada
 *
 *     (250)385-6040
 *     www.vividsolutions.com
 */
package com.vividsolutions.jts.android;

import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.drawable.shapes.Shape;

import com.vividsolutions.jts.android.geom.DrawableShape;
import com.vividsolutions.jts.android.geom.PathShape;
import com.vividsolutions.jts.android.geom.PolygonShape;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;

/**
 * Writes {@link Geometry}s into Java2D {@link Shape} objects
 * of the appropriate type.
 * This supports rendering geometries using Java2D.
 * The ShapeWriter allows supplying a {@link PointTransformation}
 * class, to transform coordinates from model space into view space.
 * This is useful if a client is providing its own transformation
 * logic, rather than relying on Java2D <tt>AffineTransform</tt>s.
 * <p>
 * The writer supports removing duplicate consecutive points
 * (via the {@link #setRemoveDuplicatePoints(boolean)} method) 
 * as well as true <b>decimation</b>
 * (via the {@link #setDecimation(double)} method. 
 * Enabling one of these strategies can substantially improve 
 * rendering speed for large geometries.
 * It is only necessary to enable one strategy.
 * Using decimation is preferred, but this requires 
 * determining a distance below which input geometry vertices
 * can be considered unique (which may not always be feasible).
 * If neither strategy is enabled, all vertices
 * of the input <tt>Geometry</tt>
 * will be represented in the output <tt>Shape</tt>.
 * <p>
 * 
 * <p>Modified for Android use.</p>
 * 
 */
public class ShapeWriter {
    /**
     * The point transformation used by default.
     */
    public static final PointTransformation DEFAULT_POINT_TRANSFORMATION = new IdentityPointTransformation();

    /**
     * The point shape factory used by default.
     */
    public static final PointShapeFactory DEFAULT_POINT_FACTORY = new PointShapeFactory.Square(3.0);

    private PointTransformation pointTransformer = DEFAULT_POINT_TRANSFORMATION;
    private PointShapeFactory pointFactory = DEFAULT_POINT_FACTORY;

    /**
     * Cache a PointF object to use to transfer coordinates into shape
     */
    private PointF transPoint = new PointF();

    /**
     * If true, decimation will be used to reduce the number of vertices
     * by removing consecutive duplicates.
     * 
     */
    private boolean doRemoveDuplicatePoints = false;

    private double decimationDistance = 0;

    /**
     * Creates a new ShapeWriter with a specified point transformation
     * and point shape factory.
     * 
     * @param pointTransformer a transformation from model to view space to use 
     * @param pointFactory the PointShapeFactory to use
     */
    public ShapeWriter( PointTransformation pointTransformer, PointShapeFactory pointFactory ) {
        if (pointTransformer != null)
            this.pointTransformer = pointTransformer;
        if (pointFactory != null)
            this.pointFactory = pointFactory;
    }

    /**
     * Creates a new ShapeWriter with a specified point transformation
     * and the default point shape factory.
     * 
     * @param pointTransformer a transformation from model to view space to use 
     */
    public ShapeWriter( PointTransformation pointTransformer ) {
        this(pointTransformer, null);
    }

    public ShapeWriter( PointTransformation pointTransformer, String shapeName, float size ) {
        this(pointTransformer, getShape(shapeName, size));
    }

    private static PointShapeFactory getShape( String shapeName, float size ) {
        if (shapeName.equals("circle")) {
            return new PointShapeFactory.Circle(size);
        } else if (shapeName.equals("cross")) {
            return new PointShapeFactory.Cross(size);
        } else if (shapeName.equals("square")) {
            return new PointShapeFactory.Square(size);
        } else if (shapeName.equals("star")) {
            return new PointShapeFactory.Star(size);
        } else if (shapeName.equals("triangle")) {
            return new PointShapeFactory.Triangle(size);
        } else if (shapeName.equals("X")) {
            return new PointShapeFactory.X(size);
        } else {
            return DEFAULT_POINT_FACTORY;
        }
    }

    /**
     * Creates a new ShapeWriter with the default (identity) point transformation.
     *
     */
    public ShapeWriter() {
    }

    /**
     * Sets whether duplicate consecutive points should be eliminated.
     * This can reduce the size of the generated Shapes
     * and improve rendering speed, especially in situations
     * where a transform reduces the extent of the geometry.
     * <p>
     * The default is <tt>false</tt>.
     * 
     * @param doDecimation whether decimation is to be used
     */
    public void setRemoveDuplicatePoints( boolean doRemoveDuplicatePoints ) {
        this.doRemoveDuplicatePoints = doRemoveDuplicatePoints;
    }

    /**
     * Sets the decimation distance used to determine
     * whether vertices of the input geometry are 
     * considered to be duplicate and thus removed.
     * The distance is axis distance, not Euclidean distance.
     * The distance is specified in the input geometry coordinate system
     * (NOT the transformed output coordinate system).
     * <p>
     * When rendering to a screen image, a suitably small distance should be used
     * to avoid obvious rendering defects.  
     * A distance equivalent to the equivalent of 1.5 pixels or less is recommended
     * (and perhaps even smaller to avoid any chance of visible artifacts).
     * <p>
     * The default distance is 0.0, which disables decimation.
     * 
     * @param decimationDistance the distance below which vertices are considered to be duplicates
     */
    public void setDecimation( double decimationDistance ) {
        this.decimationDistance = decimationDistance;
    }

    /**
     * Creates a {@link Shape} representing a {@link Geometry}, 
     * according to the specified PointTransformation
     * and PointShapeFactory (if relevant).
     * <p>
     * Note that Shapes do not
     * preserve information about which elements in heterogeneous collections
     * are 1D and which are 2D.
     * For example, a GeometryCollection containing a ring and a
     * disk will render as two disks if Graphics.fill is used, 
     * or as two rings if Graphics.draw is used.
     * To avoid this issue use separate shapes for the components.
     * 
     * @param geometry the geometry to convert
     * @return a Shape representing the geometry
     */
    public DrawableShape toShape( Geometry geometry ) {
        if (geometry.isEmpty())
            return new PathShape(new Path());
        else if (geometry instanceof Polygon)
            return toShape((Polygon) geometry);
        else if (geometry instanceof MultiPolygon)
            return toShape((MultiPolygon) geometry);
        else if (geometry instanceof LineString)
            return toShape((LineString) geometry);
        else if (geometry instanceof MultiLineString)
            return toShape((MultiLineString) geometry);
        else if (geometry instanceof Point)
            return toShape((Point) geometry);
        else if (geometry instanceof MultiPoint)
            return toShape((MultiPoint) geometry);
        else if (geometry instanceof GeometryCollection)
            return toShape((GeometryCollection) geometry);

        throw new IllegalArgumentException("Unrecognized Geometry class: " + geometry.getClass());
    }

    private DrawableShape toShape( Polygon p ) {
        PolygonShape poly = new PolygonShape();

        appendRing(poly, p.getExteriorRing().getCoordinates());
        for( int j = 0; j < p.getNumInteriorRing(); j++ ) {
            appendRing(poly, p.getInteriorRingN(j).getCoordinates());
        }

        return poly;
    }

    private void appendRing( PolygonShape poly, Coordinate[] coords ) {
        double prevx = Double.NaN;
        double prevy = Double.NaN;
        Coordinate prev = null;

        Path tmpPath = null;
        int n = coords.length - 1;
        /**
         * Don't include closing point.
         * Ring path will be closed explicitly, which provides a 
         * more accurate path representation.
         */
        for( int i = 0; i < n; i++ ) {

            if (decimationDistance > 0.0) {
                boolean isDecimated = prev != null && Math.abs(coords[i].x - prev.x) < decimationDistance
                        && Math.abs(coords[i].y - prev.y) < decimationDistance;
                if (i < n && isDecimated)
                    continue;
                prev = coords[i];
            }

            transformPoint(coords[i], transPoint);

            if (doRemoveDuplicatePoints) {
                // skip duplicate points (except the last point)
                boolean isDup = transPoint.x == prevx && transPoint.y == prevy;
                if (i < n && isDup)
                    continue;
                prevx = transPoint.x;
                prevy = transPoint.y;
            }
            if (tmpPath != null) {
                tmpPath.lineTo(transPoint.x, transPoint.y);
            } else {
                tmpPath = new Path();
                tmpPath.moveTo(transPoint.x, transPoint.y);
            }
        }
        // handle closing point
        tmpPath.close();

        Path mainPath = poly.getPath();
        if (mainPath == null) {
            poly.initPath();
            mainPath = poly.getPath();
        }
        mainPath.addPath(tmpPath);
    }

    private DrawableShape toShape( MultiPolygon mp ) {
        GeometryCollectionShape shapes = new GeometryCollectionShape();
        for( int i = 0; i < mp.getNumGeometries(); i++ ) {
            Polygon polygon = (Polygon) mp.getGeometryN(i);
            DrawableShape shape = toShape(polygon);
            shapes.add(shape);
        }
        return shapes;
    }

    private DrawableShape toShape( GeometryCollection gc ) {
        GeometryCollectionShape shape = new GeometryCollectionShape();
        // add components to GC shape
        for( int i = 0; i < gc.getNumGeometries(); i++ ) {
            Geometry g = (Geometry) gc.getGeometryN(i);
            shape.add(toShape(g));
        }
        return shape;
    }

    private PathShape toShape( MultiLineString mls ) {
        Path path = new Path();

        for( int i = 0; i < mls.getNumGeometries(); i++ ) {
            LineString lineString = (LineString) mls.getGeometryN(i);
            PathShape shape = toShape(lineString);
            path.addPath(shape.getPath());
        }
        return new PathShape(path);
    }

    private PathShape toShape( LineString lineString ) {
        Path shape = new Path();

        Coordinate prev = lineString.getCoordinateN(0);
        transformPoint(prev, transPoint);
        shape.moveTo((float) transPoint.x, (float) transPoint.y);

        double prevx = (double) transPoint.x;
        double prevy = (double) transPoint.y;

        int n = lineString.getNumPoints() - 1;
        // int count = 0;
        for( int i = 1; i <= n; i++ ) {
            Coordinate currentCoord = lineString.getCoordinateN(i);
            if (decimationDistance > 0.0) {
                boolean isDecimated = prev != null && Math.abs(currentCoord.x - prev.x) < decimationDistance
                        && Math.abs(currentCoord.y - prev.y) < decimationDistance;
                if (i < n && isDecimated) {
                    continue;
                }
                prev = currentCoord;
            }

            transformPoint(currentCoord, transPoint);

            if (doRemoveDuplicatePoints) {
                // skip duplicate points (except the last point)
                boolean isDup = transPoint.x == prevx && transPoint.y == prevy;
                if (i < n && isDup)
                    continue;
                prevx = transPoint.x;
                prevy = transPoint.y;
                // count++;
            }
            shape.lineTo((float) transPoint.x, (float) transPoint.y);
        }
        // System.out.println(count);
        return new PathShape(shape);
    }

    private DrawableShape toShape( Point point ) {
        PointF viewPoint = transformPoint(point.getCoordinate());
        return pointFactory.createPoint(viewPoint);
    }

    private DrawableShape toShape( MultiPoint points ) {
        GeometryCollectionShape shapes = new GeometryCollectionShape();
        int numGeometries = points.getNumGeometries();
        for( int i = 0; i < numGeometries; i++ ) {
            Point point = (Point) points.getGeometryN(i);
            PointF viewPoint = transformPoint(point.getCoordinate());
            DrawableShape drawableShape = pointFactory.createPoint(viewPoint);
            shapes.add(drawableShape);
        }
        return shapes;
    }

    private PointF transformPoint( Coordinate model ) {
        return transformPoint(model, new PointF());
    }

    private PointF transformPoint( Coordinate model, PointF view ) {
        pointTransformer.transform(model, view);
        return view;
    }
}
